package digitRecognitionProblem.findMlpArgs;

import genetic_algorithm.Chromosome;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

import utils.RandomGenerator;

public class MlpArgsChromosome implements Chromosome {

	public static int WEIGHTS_START = 0; // start of section encoding initial weights range
	public static int WEIGHTS_END = 6; // end of section encoding initial weights range
	public static int HIDDEN_START = WEIGHTS_END; // start of section encoding number of neurons in hidden layer
	public static int HIDDEN_END = HIDDEN_START + MlpArgsMain.LEN_HIDDEN_BITS; // end of section encoding number of neurons in hidden layer
	public static int CHROM_LEN = HIDDEN_END; // total bits in chromosome
	
	private BitSet bits; // chromosome's binary data

	/**
	 * Constructor- creates a new chromosome with random values
	 */
	public MlpArgsChromosome() {
		
		// initialize bit set
		bits = new BitSet(CHROM_LEN);
		
		// set a random value for each bit
		for (int i = 0 ; i < CHROM_LEN ; ++i) {
			if (RandomGenerator.nextInt(2) == 0) {
				bits.set(i);
			}
		}
		
		// correct invalid data
		validateBits(bits);
	}
	
	/**
	 * Constructor- creates a new chromosome with given binary data
	 * @param bits string holding chromosome's binary data
	 */
	public MlpArgsChromosome(BitSet bits) {	
		
		// initialize bit set
		int numBits = bits.size();
		this.bits = new BitSet(numBits);
		
		// set the necessary bits
		for (int i = 0 ; i < numBits ; ++i) {			
			if (bits.get(i)) {
				this.bits.set(i);
			}				
		}
		
		// correct invalid data
		validateBits(this.bits);
	} 
	
	/**
	 * Checks that bits encoding number of neurons in
	 * hidden layer are not all 0
	 * @param bits
	 */
	private static void validateBits(BitSet bits) {

		// validate weight range- cannot be 0
		while (bits.get(WEIGHTS_START, WEIGHTS_END).cardinality() == 0) {
						
			// set a random value for each bit in this subset
			for (int i = WEIGHTS_START ; i < WEIGHTS_END ; ++i) {
				if (RandomGenerator.nextInt(2) == 0) {
					bits.set(i);
				}
			}
		}
		
		// validate hidden layers number- cannot be 0
		while (bits.get(HIDDEN_START, HIDDEN_END).cardinality() == 0) {
						
			// set a random value for each bit in this subset
			for (int i = HIDDEN_START ; i < HIDDEN_END ; ++i) {
				if (RandomGenerator.nextInt(2) == 0) {
					bits.set(i);
				}
			}
		}
	}
	
	/**
	 * Returns index'th bit
	 */
	@Override
	public Object getValue(int index) throws IllegalArgumentException {

		// check boundaries
		if (index < 0 || index >= CHROM_LEN) {
			return null;			
		}
		
		// return bit at specified position
		return (Boolean) bits.get(index);
	}

	/**
	 * Flips index'th bit
	 * @param index index of bit to flip
	 * @throws IllegalArgumentException if index exceeds boundaries
	 */
	public void flipValue(int index) throws IllegalArgumentException {

		// check boundaries
		if (index < 0 || index >= CHROM_LEN) {
			throw new IllegalArgumentException("index exceeds data length");			
		}
		
		// flip specified bit
		bits.flip(index);
	}
	
	/**
	 * Calculates absolute value of range limit: [-returnValue,+returnValue].
	 * The formula is:
	 * returnValue = 2^rangeIndex / (2^(WEIGHTS_LEN+1))
	 * @param rangeIndex index of range limit within range dictated by WEIGHTS_LEN
	 * @return absolute value of range limit: [-returnValue,+returnValue]
	 */
	public static float weightsRange(int rangeIndex) {
		
		// calculate numerator
		int numerator = 2*rangeIndex;
		
		// calculate denominator
		int denominator = 2;
		for (int i = WEIGHTS_START ; i < WEIGHTS_END ; ++i) {
			denominator *= 2; 
		}
		
		// return range limit
		return (float) numerator / denominator;
	}
	
	/**
	 * Sets index'th bit to be given value.
	 * If given value is not '0' or '1' or if index exceeds
	 * boundaries, throws IllegalArgumentException
	 */
	@Override
	public void setValue(int index, Object obj) throws IllegalArgumentException {

		// check boundaries
		if (index < 0 || index >= CHROM_LEN) {
			throw new IllegalArgumentException("index exceeds data length");			
		}

		// extract bit value from input
		Boolean bit = null;
		try {			
			bit = (Boolean) obj;
		} catch (Exception e) {
			throw new IllegalArgumentException("obj is of illegal type- received " + obj.getClass());
		}
		
		// set new value
		bits.set(index, bit);
	}

	/**
	 * Return a list where i'th element is chromosome's i'th bit
	 */
	@Override
	public List<Object> getAllValues() {
		
		List<Object> retVal = new ArrayList<Object>();
		retVal.add(bits);
		return retVal;
	}
	
	/**
	 * Returns all chromosome's values
	 * @return entire binary data
	 */
	public BitSet getData() {
		return bits;
	}
	
	public String toString() {

		// decode weight range
		String output = "weights: ";
		int rangeIndex = 0;
		for (int i = MlpArgsChromosome.WEIGHTS_END - 1 ; i >= MlpArgsChromosome.WEIGHTS_START ; --i) { // read backwards
			rangeIndex += bits.get(i) ? (1 << (MlpArgsChromosome.WEIGHTS_END - 1 - i)) : 0;
		}
		float rangeLimit = MlpArgsChromosome.weightsRange(rangeIndex);
		output += "" + rangeLimit;
		
		// decode hidden layer
		int numHidden = 0;
		for (int i = MlpArgsChromosome.HIDDEN_END - 1 ; i >=  MlpArgsChromosome.HIDDEN_START ; --i) { // read backwards
			numHidden += bits.get(i) ? (1 << (MlpArgsChromosome.HIDDEN_END - 1 - i)) : 0;
		}		
		output += " ; hidden layer: " + numHidden;
		
		return output;
	}
}
